#include <GL/gl.h>

#include <cmath>
#include "cylinderPrimitive.h"
#include "defines/primitive.h"
#include "defines/axes.h"
#include "defines/rayState.h"
#include "vector.h"

#define THETA 18

CylinderPrimitive::CylinderPrimitive(int _id) : Object(_id){
}

CylinderPrimitive::CylinderPrimitive(const CylinderPrimitive &_c, int _id)
    : Object(_c.getMaterial(), _c.getCenter(), _id, CYLINDER, true, _c.getTransformation()), radius(_c.getRadius()), height(_c.getHeight()){
}

CylinderPrimitive::CylinderPrimitive(const Vector3D &_center, double _radius, double _height, const Material &_m, int _id)
    : Object(_m, _center, _id, CYLINDER, true), radius(_radius), height(_height){
}

CylinderPrimitive::CylinderPrimitive(const Vector3D &_center, double _radius, double _height, const Material &_m, const Matrix &_matrix, const Matrix &_matrixI, int _id)
    : Object(_m, _center, _id, CYLINDER, true, _matrix, _matrixI), radius(_radius), height(_height){
}

CylinderPrimitive::~CylinderPrimitive(){
}

double CylinderPrimitive::getRadius() const{
    return radius;
}

void CylinderPrimitive::setRadius(double _r){
    radius = _r;
}

double CylinderPrimitive::getHeight() const{
    return height;
}

void CylinderPrimitive::setHeight(double _h){
    height = _h;
}

double CylinderPrimitive::volume() const{
    return M_PI*radius*radius*height;
}

double CylinderPrimitive::area() const{
    return (M_PI*radius*radius*2.0) + (M_PI*radius*height*2.0);
}

void CylinderPrimitive::draw(bool _wireframe, bool _useParentColor, const Material &_parentColor) const{
    if(_wireframe){
//        glDisable(GL_LIGHT0);
//        glDisable(GL_LIGHTING);

        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glDisable(GL_CULL_FACE);

//        glLineWidth(2);
    }

	glPushMatrix();
	glMultMatrixd(transformation.getLocalToWorld().toGlMatrix());

    Material color_ = chooseMaterial(_wireframe, _useParentColor, _parentColor);
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, color_.getAmbientGL());
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color_.getDiffuseGL());
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, color_.getSpecularGL());


	//desenhando a base em cima
    glBegin(GL_POLYGON);
    glNormal3f(0, 0, 1);

    for(double i=0; i<=360.0; i = i+THETA){
        double theta = toRad(i);
        double x = radius*cos(theta);
        double y = radius*sin(theta);

        glVertex3f(x, y, height);
    }
    glEnd();


    glBegin(GL_QUAD_STRIP);
    Vector3D p1, p2, p3, normal;
    for(double i=0; i<=360.0; i = i+THETA){
        double theta = toRad(i);

        double x = radius*cos(theta);
        double y = radius*sin(theta);
        p1 = Vector3D(x, y, height);
        p2 = Vector3D(x, y, 0);

        theta = toRad(i + THETA);
        x = radius*cos(theta);
        y = radius*sin(theta);
        p3 = Vector3D(x, y, center.z + height);

        normal = ((Vector3D(0, 0, -1)).crossProduct(p3-p1)).normalize();

        glNormal3f(normal.x, normal.y, normal.z);

        glVertex3f(p1.x, p1.y, p1.z);
        glVertex3f(p2.x, p2.y, p2.z);
    }
    glEnd();

	//desenhando a base em baixo
    glBegin(GL_POLYGON);
    glNormal3f(0, 0, -1);

    for(double i=0; i<=360.0; i = i+THETA){
        double theta = toRad(-i);
        double x = radius*cos(theta);
        double y = radius*sin(theta);

        glVertex3f(x, y, 0);
    }
    glEnd();


	glPopMatrix();


    if(_wireframe){
//        glLineWidth(1);
//
//        glEnable(GL_LIGHT0);
//        glEnable(GL_LIGHTING);

        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        glEnable(GL_CULL_FACE);
    }


    if(selected){
        Vector3D min, max;
        getMinMax(&min.x, &max.x, X_AXIS);
        getMinMax(&min.y, &max.y, Y_AXIS);
        getMinMax(&min.z, &max.z, Z_AXIS);

        drawBox(true, min - center, max - center);
    }
}

std::vector< Trio > CylinderPrimitive::intersection(const Ray &_ray) const{
    std::vector< Trio > result;

    Vector3D origin = _ray.getOrigin();
    //inserindo o primeiro ponto que TODOS terão
    result.push_back( Trio(origin, Vector3D(1,0,0), OUT));

    //colocnado o raio para coordenadas da primitiva
    Matrix toLocalVector = transformation.getWorldToLocal();
    Matrix toLocalDirection = transformation.getWorldToLocalDirection();

    origin = (toLocalVector * origin.toHomogeneousCoordinates()).fromHomogeneousCoordinates();
    Vector3D direction = Vector3D(toLocalDirection * Vector(_ray.getDirection()));


//    std::cout << "Tem os vetores origem e direção nas coordenadas locais"<<std::endl;
//    std::cout << "Origem" <<std::endl;
//    origin.print();
//    std::cout << "Direção" <<std::endl;
//    direction.print();
//
//    std::cout << "Centro" <<std::endl;
//    center.print();
//    Vector3D originCenter = origin - center;

    //------------------------------------- interseção com o cilindro infinito
    double a = (direction.x * direction.x) + (direction.y * direction.y);
    double b = ((direction.x * origin.x) + (direction.y * origin.y)) * 2.0;
    double c = (origin.x * origin.x) + (origin.y * origin.y) - (radius*radius);
    //*** aqui poderia não precisar o raio ao quadrado se tivesse a trasnformação

    double delta = (b*b) - (4.0 * a * c);
//
//    std::cout << "\nValores\n\tA = " <<a <<"\n\tB = " << b << "\n\tC = " << c << "\n\tDelta = " <<delta<<std::endl;


    std::vector<Trio> possibleCases;

    if(fabs(direction.z) >= ERROR){
//        std::cout <<"Adicionando tampas" <<std::endl;
        double tdiscMin = - origin.z / direction.z;

        if(tdiscMin > -ERROR ){//é maior ou igual a zero
            Vector3D localP = origin + (direction * tdiscMin);

            //checar se o ponto passa dentro do circulo
            Vector3D pxy(localP.x, localP.y, 0);
            if(pxy.dotProduct(pxy) - ERROR <= radius * radius ){
                possibleCases.push_back(Trio(tdiscMin, localP, Vector3D(0,0,-1)));
            }
        }

        double tdiscMax = (height - origin.z) / direction.z;

        if(tdiscMax > -ERROR ){//é maior ou igual a zero
            Vector3D localP = origin + (direction * tdiscMax);

            //checar se o ponto passa dentro do circulo
            Vector3D pxy(localP.x, localP.y, 0);
            if(pxy.dotProduct(pxy) - ERROR <= radius * radius ){

                if(tdiscMax > -ERROR){ //se t == 0
    //                std::cout << "inserido t unico " << t << std::endl;
                    bool inserted = false;
                    for(unsigned int i = 0; i < possibleCases.size(); i++){
                        if(tdiscMax < possibleCases.at(i).t){
    //                        Vector3D p = (toWorld * (localP).toHomogeneousCoordinates()).fromHomogeneousCoordinates();
                            possibleCases.insert(possibleCases.begin() + i, Trio(tdiscMax, localP, Vector3D(0,0,1)));

                            inserted = true;
                            break;
                        }
                    }
                    if(!inserted){
    //                    Vector3D p = (toWorld * (localP).toHomogeneousCoordinates()).fromHomogeneousCoordinates();
                        possibleCases.push_back(Trio(tdiscMax, localP, Vector3D(0,0,1)));
                    }
                }
            }
        }
    }


    if(fabs(delta) < ERROR){ //delta igual a zero
//        std::cout <<"Adicionando delta = 0" <<std::endl;
        if(fabs(a) >= ERROR){//sençao seria acrescentado os dois pontos anteriormente
            double t = - b / (2.0 * a);
            Vector3D localP = origin + (direction * t);
            Vector3D n = normal(localP);

//            std::cout << "Delta 0" <<std::endl;

            if( (localP.z + ERROR > 0.0) && (localP.z - ERROR < height) ){
                if(t > -ERROR){ //se t == 0
                    bool inserted = false;
                    for(unsigned int i = 0; i < possibleCases.size(); i++){
                        if(t < possibleCases.at(i).t){
                            possibleCases.insert(possibleCases.begin() + i, Trio(t, localP, n));

                            inserted = true;
                            break;
                        }
                    }
                    if(!inserted){
                        possibleCases.push_back(Trio(t, localP, n));
                    }
                }
            }
        }
    }else if(delta > 0.0){
//        std::cout <<"Adicionando delta > 0" <<std::endl;
        double t0 = (- b - sqrt(delta)) / (2.0 * a);
        double t1 = (- b + sqrt(delta)) / (2.0 * a);


        Vector3D localP0 = origin + (direction * t0);
        Vector3D localP1 = origin + (direction * t1);
        Vector3D n0 = normal(localP0);
        Vector3D n1 = normal(localP1);

        unsigned int i = 0;
        if( (localP0.z + ERROR > 0.0) && (localP0.z - ERROR < height) ){// se tiver entre os z's
            if(t0 > - ERROR){//se for positivo
                bool inserted = false;
                for(; i < possibleCases.size(); i++){
                    if(t0 < possibleCases.at(i).t){
                        possibleCases.insert(possibleCases.begin() + i, Trio(t0, localP0, n0));
                        inserted = true;
                        i++;
                        break;
                    }
                }
                if(!inserted){
                    possibleCases.push_back(Trio(t0, localP0, n0));
                    i++;
                }
            }
        }

        if( (localP1.z + ERROR > 0.0) && (localP1.z - ERROR < height) ){
            if(t1 > - ERROR){//se for positivo
                bool inserted = false;
                for(; i < possibleCases.size(); i++){
                    if(t1 < possibleCases.at(i).t){
                        possibleCases.insert(possibleCases.begin() + i, Trio(t1, localP1, n1));

                        inserted = true;
                        i++;
                        break;
                    }
                }
                if(!inserted){
                    possibleCases.push_back(Trio(t1, localP1, n1));
                }
            }
        }
    }

//    std::cout <<"Existem " << possibleCases.size() << " casos" <<std::endl;

    Matrix toWorld = transformation.getLocalToWorld();
    Matrix toLocalTranpost = transformation.getWorldToLocalDirection().transpose();
    switch(possibleCases.size()){
        case 0:{
            break;
        }
        case 1:{//casos: saindo de dentro do cilindro e saindo pelo lado ou pela tampa; raspando ao lado do cilindro

            if( fabs(direction.z) < ERROR ){
                if( (fabs(possibleCases.at(0).point.z) < ERROR ) || ( fabs(possibleCases.at(0).point.z - height) < ERROR )){

//                    result.back().type = ON;
//                    result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
                    result.push_back(Trio((toWorld * (possibleCases.front().point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                          Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), ON));
                }else{
                    result.back().type = IN;
                }
                result.push_back(Trio((toWorld * (possibleCases.front().point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                      Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), OUT));
                break;
            }

            Vector3D originXY(origin.x, origin.y, 0);
            if( ((origin.z > -ERROR) && (origin.z - height < ERROR )) && (originXY.dotProduct(originXY) - ERROR <= radius * radius) )
                result.back().type = IN;
            else
                result.push_back(Trio((toWorld * (possibleCases.front().point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                      Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), ON));

            if( fabs(possibleCases.front().t) < ERROR ){
                result.back().type = ON;
                result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
            }

            result.push_back(Trio((toWorld * (possibleCases.front().point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), OUT));

            break;
        }
        case 2:{//casos: passando pelo meio(in), passando pelas bases(on), passando na vertical, saindo do meio e indo na quina
            if( fabs(a) < ERROR ){
                if( fabs(possibleCases.at(0).t) < ERROR ){
                    result.back().type = ON;
                    result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
                }
                Vector3D localP = possibleCases.at(0).point;
                Vector3D pxy(localP.x, localP.y, 0);
                if(fabs(pxy.dotProduct(pxy) - radius * radius) < ERROR )
                    result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                          Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), ON));
                else
                    result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                          Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), IN));
                result.push_back(Trio((toWorld * (possibleCases.at(1).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                      Vector3D(toLocalTranpost * Vector(possibleCases.at(1).normal)).normalize(), OUT));
                break;
            }
            if( fabs(direction.z) < ERROR ){
                if( (fabs(possibleCases.at(0).point.z) < ERROR ) || ( fabs(possibleCases.at(0).point.z - height) < ERROR ))
                    result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                          Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), ON));
                else
                    result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                          Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), IN));
                result.push_back(Trio((toWorld * (possibleCases.at(1).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                      Vector3D(toLocalTranpost * Vector(possibleCases.at(1).normal)).normalize(), OUT));
                break;
            }

            Vector3D originXY(origin.x, origin.y, 0);
            if( ((origin.z > -ERROR) && (origin.z - height < ERROR )) && (originXY.dotProduct(originXY) - ERROR <= radius * radius) )
                result.back().type = IN;

            if( fabs(possibleCases.at(0).t) < ERROR ){
                result.back().type = ON;
                result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
            }
            result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), IN));

            if( fabs(possibleCases.at(0).t - possibleCases.at(1).t) < ERROR ){
                result.back().type = ON;
                result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
            }else if( (fabs(possibleCases.at(0).point.z + height) < ERROR) && (fabs(possibleCases.at(1).point.z + height) < ERROR) ) {
                result.back().type = ON;
                result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
            }
            result.push_back(Trio((toWorld * (possibleCases.at(1).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(1).normal)).normalize(), OUT));
            break;
        }
        case 3:{//casos: passando por um lado ou baso, e na quina, ou o contrario
            if( fabs(possibleCases.at(0).t) < ERROR ){
                result.back().type = ON;
                result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
            }
            result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), IN));

            result.push_back(Trio((toWorld * (possibleCases.at(2).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(2).normal)).normalize(), OUT));

            break;
        }
        case 4:{//casos: passando por udas quinas
            if( fabs(possibleCases.at(0).t) < ERROR ){
                result.back().type = ON;
                result.back().normal = Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize();
            }
            result.push_back(Trio((toWorld * (possibleCases.at(0).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(0).normal)).normalize(), IN));

            result.push_back(Trio((toWorld * (possibleCases.at(3).point).toHomogeneousCoordinates()).fromHomogeneousCoordinates(),
                                  Vector3D(toLocalTranpost * Vector(possibleCases.at(3).normal)).normalize(), OUT));
            break;
        }
    }


    return result;
}

void CylinderPrimitive::getMinMax(double *_min, double *_max, unsigned int _axis) const{
    Matrix toWorld = transformation.getLocalToWorld();
    switch(_axis){
        case X_AXIS:{
            *_min = center.x - radius;
            *_max = center.x + radius;

//            Vector3D minVector(*_min, 0, 0);
//            Vector3D maxVector(*_max, 0, 0);
//            *_min = (toWorld * (minVector).toHomogeneousCoordinates())[0];
//            *_max = (toWorld * (maxVector).toHomogeneousCoordinates())[0];

            break;
        }
        case Y_AXIS:{
            *_min = center.y - radius;
            *_max = center.y + radius;
//
//            Vector3D minVector(0, *_min, 0);
//            Vector3D maxVector(0, *_max, 0);
//            *_min = (toWorld * (minVector).toHomogeneousCoordinates())[1];
//            *_max = (toWorld * (maxVector).toHomogeneousCoordinates())[1];
            break;
        }
        case Z_AXIS:{
            *_min = center.z;
            *_max = center.z + height;

//            Vector3D minVector(0, 0, *_min);
//            Vector3D maxVector(0, 0, *_max);
//            *_min = (toWorld * (minVector).toHomogeneousCoordinates())[2];
//            *_max = (toWorld * (maxVector).toHomogeneousCoordinates())[2];
            break;
        }
    }
}

Vector3D CylinderPrimitive::normal(const Vector3D &_p) const{
//    Vector3D centerXY(center.x, center.y, 0.0);
    Vector3D pXY(_p.x, _p.y, 0.0);

    return (pXY).normalize();
}
